const log = require('../utils/logger')
const _ = require('lodash')

class ExecutorClient {
    logger
    logPath
    logFile
    #id = 1
    #cache = new Map()
    disposable = []
    constructor(opts) {
        this.logPath = opts.logPath
        this.logFile = opts.logFile
        if (this.logPath && this.logFile) {
            const name = this.logFile + '-client.txt'
            this.logger = log.create(this.logPath, name)
            this.disposable.push(() => {
                log.close(name)
            })
        }
    }
    #decodeArgs(args) {
        return args.map(it => {
            if (it.type == 'FUNCTION') {
                return (...params) => {
                    this.#send(it.id, 'ARGS_FUNCTION', params)
                }
            }
            return it
        })
    }
    #encodeReturn(response) {
        if (_.isFunction(response)) {
            return {
                id,
                type: 'FUNCTION'
            }
        }
        if (_.isPlainObject(response)) {
            return _.mapValues(response, value => {
                if (_.isFunction(value)) {
                    const id = this.#id++
                    this.#cache.set(id, value.bind(response))
                    return {
                        id,
                        type: 'FUNCTION'
                    }
                }
                return value
            })
        }
        return response
    }
    #createMessage(id, command, data) {
        const message = {
            id,
            command,
            data
        }
        return message
    }
    async #send(id, command, data) {
        const msg = this.#createMessage(id, command, data)
        this.onSend(msg)
    }
    async on(message) {
        const { id, command, data, metedata } = message
        let result
        const args = this.#decodeArgs(data)
        try {
            switch (command) {
                case 'RETURN_FUNCTION':
                    const [fnId, ...params] = args
                    const fn = this.#cache.get(fnId)
                    result = await fn?.(...params)
                    break
                case 'CLOSE':
                    await this.logger?.info('执行器已经关闭, 资源已经释放...')
                    for (const dispose of this.disposable) {
                        await dispose()
                    }
                    break
                default:
                    result = await this.onExec(metedata, command, ...args)
                    break
            }
            const response = this.#encodeReturn(result)
            this.#send(id, 'FINISH', response)
        } catch (error) {
            this.#send(id, 'ERROR', {
                message: error.message,
                stack: error.stack
            })
        }
    }
    async onExec(metedata, command, ...args) {
        this.logger?.info({ command, args, metedata })
        const module = require(metedata.scriptPath)
        if (_.isFunction(module)) {
            const ctx = {
                ...global,
                _metedata: metedata
            }
            if (command) {
                return await module.call(ctx, command, ...args)
            }
            return await module.call(ctx, ...args)
        }
        if (_.isPlainObject(module)) {
            module._metedata = metedata
            return await module[command]?.(...args)
        }
        throw new Error('脚本导出的不是方法也不是普通对象, 请检查脚本: ' + metedata.scriptPath)
    }
    async onInit() {}
    async onSend() {}
}

module.exports = ExecutorClient
